Chapter 5
Custom Control Development

by Ed Harris

In This Chapter

  Window Classes Versus C++ Classes 214
  A Validating Edit Control 215
  The Clock Static Control 217
  The Hyperlink Control 225
  Advanced Custom Control Topics 237

Custom controls, or widgets, are useful when the standard input and display controls cannot provide the user with the most optimal input mechanism. Not too long ago, Windows featured a bare-minimum control palette—static text control, button, edit box, and list box. The combo box, now considered a basic staple of user interface, was a welcome addition to Windows 3.

Now, of course, there are dozens of built-in controls, and hundreds of third-party add-ons. This chapter focuses on the whens, whys, and hows of extending stock controls and developing brand new controls of your own.


Note:  

This chapter covers the development techniques needed to create new window behavior. What it doesn’t cover is the wisdom—and the usability testing—required to determine whether new window behavior is warranted. Sure, your new widget may display editable information to the user in a highly efficient manner. However, if the user can’t understand how to manipulate the control without breaking out the documentation, your application becomes unusable. Make sure that the user has the proper affordances to use your new widget. (Affordance is GUI usability-speak for intuitive understanding.) Generally, it’s a good idea not to stray too far from the beaten path.


Window Classes Versus C++ Classes

One of the confusing aspects of custom control development is the overloaded use of the word class. When Windows was introduced, class was an underused term, and C++ was merely a toy used by AT&T labs. The Windows architects chose to use the term class to represent the collective behavior of a set of windows. Window class attributes include the class name (such as Edit), icon, background color, cursor, and window procedure.

Object-oriented languages use the term class in a similar way, to identify the set of behavior that a family of code (the class) provides. These two uses overlap in the area of custom control development because the programmatic class is used to implement the behavior of the window class.

The term subclass, in object parlance, means a new class (a child) derived from one or more existing classes. In this naming scheme, the parent class is called the superclass.

In the Windows world, subclassing is the action of modifying the behavior of an existing window. Subclassing is done on a window-by-window basis. Superclassing is the act of creating a new breed of window based on the behavior of an existing type (class) of window. As you will see, subclassing is done extensively by MFC. Although superclassing is possible with MFC, it has some significant drawbacks.

In pure Windows-API based development, window class behavior is provided by the message procedure (message proc). Each message destined for a particular window is sent to a single function, where it is routed to code specific to that message. In the early days of Windows development, message procedures sometimes grew to be thousands of lines long.

MFC replaced the message procedure with the concept of a message map. When an MFC-owned window receives a message, the framework looks up the message map for that window, and routes it to the most-derived handler for that message. MFC uses a single window procedure (AfxWndProc) to receive all messages and route them to the appropriate code. When you call CreateWindow, CreateDialog, or any other API that causes a window to be instantiated, you are subclassing that window procedure with AfxWndProc. From that point on, all messages from the system to that window are routed through the message map for processing. This simplifies subclassing enormously and removes the most tedious and error-prone parts of the process.

When creating a custom class, consider whether an existing class provides any of the functionality you need. For example, if the class requires textual input, subclassing the standard edit class might be appropriate. Extending an existing class is generally much easier than creating a new one from scratch.

A Validating Edit Control

Perhaps the seminal subclass example is an edit control that validates or reformats its contents. This control, CZipEdit, will format zip codes into ZIP+4 format when focus leaves the control. The control contains one message map handler, for the WM_KILLFOCUS (OnKillFocus) method.

class CZipEdit : public CEdit
{
    // Implementation
    public:
	virtual void    FormatContents  (const TCHAR* pszText);

    // Message Map
    public:
	//{{AFX_MSG(CZipEdit)
    afx_msg void    OnKillFocus    (CWnd* pwndNew);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

void CZipEdit::OnKillFocus (CWnd* pwndNew)
{
    CString strText;
    GetWindowText (strText);

    FormatContents (strText);

    CEdit::OnKillFocus (pwndNew);
}

The kill focus message handler retrieves the window text, passes it to the FormatContents method, and then invokes the default CEdit kill focus handler. The processing is done prior to the default code so that the new edit control contents (if any) are available to the application when the EN_KILLFOCUS notification is received.

The function FormatContents is responsible for taking a character string, doing any necessary transformations, and then setting the resulting text into the edit control. It has public scope; this allows it to be used as a formatting replacement for SetWindowText.

The implementation for the zip code is rather trivial. It verifies that the first five characters of the zip code are digits, and then inserts a single dash at the fifth character.

void CZipEdit::FormatContents (const TCHAR* pszText)
{
    CString strOutput (pszText);

    if (strOutput.GetLength() >= 9)
    {
	BOOL bValidZip = TRUE;
	for (int nDigit = 0; nDigit < 5; nDigit++)
	{
	    if (!isdigit (strOutput[nDigit]))
		bValidZip = FALSE;
	}

	if (bValidZip && strOutput[5] != _T(‘-’))
	{
	    if (!ispunct (strOutput[5]) && !isspace (strOutput[5]))
		strOutput = strOutput.Left (5) + _T(‘-’) +
		ÄstrOutput.Mid (5);
	    else
		strOutput.SetAt (5, _T(‘-’));
	}
    }

    SetWindowText (strOutput);
}



To convert a dialog box edit control into a formatting edit control, use ClassWizard to bind a CEdit object to an edit control. Then, in your dialog box header file, replace the CEdit object with a CZipEdit object. When MFC binds the control to the window, it subclasses the control and the formatting behavior is enabled.

The Clock Static Control

The second custom control, CClockCtrl, displays an analog clock or stopwatch face (see Figure 5.1). Because it is a display-only control, accepting neither keyboard nor mouse input, it is derived from CStatic. Patterned on the analog face of the Windows clock accessory application, this control has the following features:

  The control will shrink or grow to fill the entire window area.
  The window will contain colored studs at each of the 12 face points. If the control is large enough, each minute mark will have a smaller stud as well. The control will dynamically determine whether the client area is large enough to contain the minute marks.
  The control will display up to three hands: an hour hand, a minute hand, and a second hand. The hands will be drawn in such a way that there is no discernible flicker if the control is used to display real time.
  The control will not have any timing built in. The user of the clock will need to set the time as appropriate. Thus, the control can be used as a countdown timer, count-up timer, clock, or static display.


Figure 5.1  An analog clock face created with the CClockCtrl class.

Here is the class definition for the complete clock control:

class CClockCtrl : public CStatic
{
    // Internal constants
    enum HANDTYPE { HOUR_HAND, MINUTE_HAND, SECOND_HAND };

    // Ctor / dtor
    public:
	CClockCtrl();

    // Members
    protected:
	COLORREF    m_rgbHands;         // Hand color
	COLORREF    m_rgbPoints;        // Point color
	CPoint      m_ptMiddle;         // Center point of window
	int         m_nPointWidth;      // Width of face point
	int         m_nRadius;          // Radius of circular portion
					            // of window
	int         m_nHour;            // Hour hand value
	int         m_nMinute;          // Minute hand value
	int         m_nSecond;          // Second hand value

    // API
    public:
	BOOL CreateFromStatic   (CWnd* pwndParent, UINT wID);
	void SetTime            (int nHour, int nMinute, int nSecond);

    // Members
    protected:
	void    RecalcLayout        (void);
	CPoint  ComputeFacePoint    (int nMinute,
					int nFaceLength) const;
	void    GetHandPoints       (int nFaceValue, HANDTYPE typeHand,
					CPoint* pptHand);
	void    DrawFacePoint       (CDC& dc, const CPoint& ptFace,
					BOOL bMajor);
	void    DrawHand            (CDC& dc, int nFaceValue,
					HANDTYPE typeHand, BOOL bDraw);

    // Message Map
    public:
	//{{AFX_MSG(CClockCtrl)
	afx_msg void    OnSize      (UINT nType, int cx, int cy);
	afx_msg void    OnPaint     (void);
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()
};

Control Metrics

One of the stated requirements for the clock control is that it grow to fill its window area. Because my trigonometry isn’t that good, and because I don’t enjoy lopsided clocks, the control “squares” itself up within the window. All of the metrics are set in a single function, RecalcLayout. For the clock control, you track three items: the center of the clock face, the maximum radius that can be inscribed in the circle, and the face point width. The need for the center point and radius should be reasonably apparent; the point width is the width of one of the hash marks on the outside of the clock.

void CClockCtrl::RecalcLayout (void)
{
    CRect rectClient;
    GetClientRect (&rectClient);

    // Square off the control and determine radius
    m_ptMiddle.x = rectClient.Width() / 2;
    m_ptMiddle.y = rectClient.Height() / 2;
    m_nRadius = min (m_ptMiddle.x, m_ptMiddle.y);

    // Point width is used to determine hash-mark widths
    m_nPointWidth = (int) m_nRadius / 20;
    if (m_nPointWidth < 2)
	m_nPointWidth = 2;

    Invalidate (TRUE);

    return;
}

To limit the proliferation of math throughout the control, the trigonometric calculations have been contained in a single routine. This routine, ComputeFacePoint, takes a minute value and a face length (generally a fraction of the radius). It returns a CPoint, in client window coordinates, of the specified point. Because the control is being used for time display, it was easier to accept a minute value than an angle in degrees.

CPoint CClockCtrl::ComputeFacePoint (int nMinute, int nFaceLength) const
{
    CPoint ptCalc;

    // Convert minutes to degrees
    double fDegrees = 180 + ((15 + nMinute) % 60) * 6;

    // Begin conversion to radians
    double fAngle = fDegrees / 180;
    ptCalc.x = m_ptMiddle.x + (int) (cos (fAngle * pi) * nFaceLength);
    ptCalc.y = m_ptMiddle.y + (int) (sin (fAngle * pi) * nFaceLength);

    return (ptCalc);
}

Painting the Face

With the basic control math out of the way, you can go ahead and draw the surface of the clock. This is done in the OnPaint handler. The painting routine is pretty straightforward: You allocate a device context and brush for the face points, and then inscribe each face point from 12 o’clock clockwise through 11 o’clock. Each face point is located 90 percent out from the midpoint, using a function DrawFacePoint. DrawFacePoint accepts a device context (DC), the face-point location, and a Boolean indicating whether the point should be drawn in large format (the o’clock points) or small format (the minute hashes).

void CClockCtrl::OnPaint (void)
{
    // Force initialized
    if (m_nRadius == -1)
	RecalcLayout ();

    CPoint      ptFace;
    CPaintDC    dc (this);

    CBrush brPoint (m_rgbPoints);
    CBrush* pbrOld = dc.SelectObject (&brPoint);

    // The face points go 90% out from the radius
    int nFaceLength = MulDiv (m_nRadius, 9, 10);

    // Inscribe a circle from 12 O’clock clockwise in radians
    for (int nMinute = 0; nMinute < 60; nMinute++)
    {
	ptFace = ComputeFacePoint (nMinute, nFaceLength);
	DrawFacePoint (dc, ptFace, ((nMinute % 5) == 0) ? TRUE : FALSE);
    }

    DrawHand (dc, m_nHour, HOUR_HAND, TRUE);
    DrawHand (dc, m_nMinute, MINUTE_HAND, TRUE);
    DrawHand (dc, m_nSecond, SECOND_HAND, TRUE);

    (void) dc.SelectObject (pbrOld);

    return;
}
void CClockCtrl::DrawFacePoint (CDC& dc, const CPoint& ptFace,
ÄBOOL bMajor)
{
    CRect rectPoint (ptFace.x, ptFace.y, ptFace.x, ptFace.y);

    if (bMajor)
    {
	rectPoint.InflateRect ((m_nPointWidth / 2) + 1,
			   	      (m_nPointWidth / 2) + 1);

	dc.Rectangle (&rectPoint);
	dc.Draw3dRect (&rectPoint, GetSysColor (COLOR_BTNHIGHLIGHT),
				       GetSysColor (COLOR_BTNSHADOW));

    }
    else
    {
	if (m_nPointWidth > 2)
	{
	    rectPoint.InflateRect (1, 1);
	    dc.Draw3dRect (&rectPoint, GetSysColor (COLOR_BTNHIGHLIGHT),
					GetSysColor (COLOR_BTNSHADOW));

	}
    }

    return;
}



DrawFacePoint uses the previously calculated point width value to determine the width of the hashmarks. The function CDC::Draw3dRect is used to draw a bevel around the hashpoint: a highlight on the upper-left side of the hash and a shadow on the lower-right side. This single function saves dozens of lines of code allocating pens and computing start and stop points for the beveling.

Locating the Hands

The final drawing-related code for the clock control is the hand-painting scheme. After looking at several clocks, I decided that the hands should be a simple polygon extending from either side of the midpoint. Because the control needs to scale to different sizes, the entire math is done as a multiple of the radius and the point width. The hour hand extends to 50 percent of the radius, the minute hand extends to 70 percent, and the second hand 80 percent.

The approach I took in drawing the hour and minute hands leverages the face point location math. Each of the four vertices of the hand is calculated as the hand angle plus a constant. Consider a hand pointing at the 12 o’clock position. To draw the hand, you start from the 6 o’clock position, a little behind the midpoint. From there, you draw a line to the 3 o’clock position, parallel to the midpoint. After that, you go to the 12 o’clock position (the actual point of the hand). The last point is at the 9 o’clock position (again, parallel to the midpoint). This strategy simplified the hand drawing tremendously.

Not to be left out, the second (or sweep) hand is drawn as a thin (single-pixel) line extending out from the midpoint.

void CClockCtrl::GetHandPoints (int nValue, HANDTYPE typeHand,
ÄCPoint* pptHand)
{
    int     nLength = 0;

    switch (typeHand)
    {
	case HOUR_HAND:
	    nLength = MulDiv (m_nRadius, 50, 100);  // 50% of radius
	    // Convert the hour value (0-11) to a minute value (0-59)
	    // for drawing, then adjust for a gradual transition from
	    // hour to hour.
	    nValue *= 5;
	    nValue += (m_nMinute / 12);
	    break;
	case MINUTE_HAND:
	    nLength = MulDiv (m_nRadius, 70, 100);  // 70% of radius
	    break;
	case SECOND_HAND:
	    nLength = MulDiv (m_nRadius, 80, 100);  // 80% of radius
	    break;
	default:
	    ASSERT (FALSE);
    }

    if (typeHand == HOUR_HAND || typeHand == MINUTE_HAND)
    {
	// Compute the hand points.  First point is the back side,
	// second point is the right, third point is the tip,
	// and fourth is left.
	pptHand[0] = ComputeFacePoint (nValue + 30, m_nPointWidth * 2);
	pptHand[1] = ComputeFacePoint (nValue + 15, m_nPointWidth);
	pptHand[2] = ComputeFacePoint (nValue,      nLength);
	pptHand[3] = ComputeFacePoint (nValue - 15, m_nPointWidth);
    }
    else
    {
	pptHand[0] = m_ptMiddle;
	pptHand[1] = ComputeFacePoint (nValue, nLength);
    }
}

Painting the Hands

The first attempt at painting the hands was pretty simplistic. I invalidated the area underneath the hands, and then waited for a WM_PAINT to be generated to update the window. Unfortunately, this caused far too many flickers in the window, as each paint message caused the middle of the face to be filled with the background, and then painted.

To remove the flicker, the control needed to take care of both the erasing and redrawing of the hands. For draw operations, the hand color (currently a light blue) is used to fill the hand polygon; for erase operations, the hands are drawn in the background color.

The sweep hand is drawn using a NOT XOR raster operation (ROP) code. The NOT XOR ROP causes the hand to be visible as a combination of the line and the existing background. An additional call with this ropcode erases the line. This was key to being able to update the control quickly, as the sweep hand generally moves every second.

void CClockCtrl::DrawHand (CDC& dc, int nValue,
ÄHANDTYPE typeHand, BOOL bDraw)
{
    COLORREF rgbBrush;
    COLORREF rgbPen;
    CPoint ptHand[4];

    if (nValue == HIDE_HAND)
	return;

    GetHandPoints (nValue, typeHand, ptHand);
    if (typeHand == HOUR_HAND || typeHand == MINUTE_HAND)
    {
	DrawHand (dc, m_nSecond, SECOND_HAND, FALSE);

	rgbBrush = (bDraw) ? m_rgbHands : GetSysColor (COLOR_BTNFACE);
	rgbPen   = (bDraw) ? RGB (0, 0, 0) : GetSysColor (COLOR_BTNFACE);

	CBrush  brHand (rgbBrush);
	CPen    penHand (PS_SOLID, 1, rgbPen);

	CBrush* pbrOld  = dc.SelectObject (&brHand);
	CPen*   ppenOld = dc.SelectObject (&penHand);

	dc.Polygon (ptHand, 4);

	(void) dc.SelectObject (pbrOld);
	(void) dc.SelectObject (ppenOld);

	DrawHand (dc, m_nSecond, SECOND_HAND, TRUE);
    }
    else
    {
	int noldROP = dc.SetROP2 (R2_NOTXORPEN);

	dc.MoveTo (ptHand[0]);
	dc.LineTo (ptHand[1]);

	(void) dc.SetROP2 (noldROP);
    }

    return;
}

Setting the Time

The last function in the clock control is used to set the time displayed by the hands. The hour, minute, and second values are set independently of each other—either as a numeric value or as the constant HIDE_HAND. To avoid flicker, remove the old hands, and then redraw the new ones.

void CClockCtrl::SetTime (int nHour, int nMinute, int nSecond)
{
    CClientDC dc (this);

    // Hour or minute is changing.  Erase both
    if (m_nHour != nHour || m_nMinute != nMinute)
    {
	DrawHand (dc, m_nSecond, SECOND_HAND, FALSE);

	m_nSecond = -1; // Inhibit second hand drawing
	DrawHand (dc, m_nMinute, MINUTE_HAND, FALSE);
	DrawHand (dc, m_nHour,   HOUR_HAND,   FALSE);

	// Update the internals
	m_nHour   = nHour % 12;
	m_nMinute = nMinute % 60;

	DrawHand (dc, m_nHour,   HOUR_HAND,  TRUE);
	DrawHand (dc, m_nMinute, MINUTE_HAND, TRUE);

	m_nSecond = nSecond % 60;
	DrawHand (dc, m_nSecond, SECOND_HAND, TRUE);
    }
    else
    {
	DrawHand (dc, m_nSecond, SECOND_HAND, TRUE);
	m_nSecond = nSecond % 60;
	DrawHand (dc, m_nSecond, SECOND_HAND, TRUE);
    }

    ValidateRect (NULL);
}

Pitfalls of Subclassing Standard Controls

One of the problems of subclassing existing controls is that you must understand the default behavior very thoroughly. In some cases, you might need to intercept messages prior to the default processing. In others, you might need to do additional processing after default processing. Finally, in some cases you might need to eat messages entirely, substitute additional ones, or both. The best tool for understanding existing behavior is Spy++. Spy++ displays the messages a window receives and even provides user decodes to common messages.

If no close match exists, subclass CWnd instead. When you subclass CWnd, you inherit no behaviors other than those provided by Windows. It’s like starting with a blank sheet of paper.



The Hyperlink Control

The second custom control is the CHyperlink control. The CHyperlink appears like a static window with underlined text, as shown in Figure 5.2. Unlike a static window, however, the hyperlink control accepts keyboard and mouse input. If the user clicks on the hyperlink or hits Enter while the hyperlink has focus, the text of the link is launched via the shell. The hyperlink control derives directly from CWnd.


Figure 5.2  An example of the CHyperLink control with three fonts.

The first step of the custom control process is defining the observable behavior of the control:

  All text in the control will be displayed with an underlined version of the current font. If the user passes a font to the control through the SetFont() API, the control creates an underlined version. This simplifies using the class because you won’t have to make your own underlined copies of fonts.
  By default, the link is displayed in light blue. The color is not changed if the user clicks on the link (link tracking is left as an exercise for the reader). The user can control the display color programmatically.
  When the mouse is over the textual part of the control, a hand cursor is displayed. At this point, clicking on the underlined text causes the appropriate registered application to be launched via the shell. If the mouse is not over the textual portion, the arrow cursor is displayed.
  Because the control accepts mouse input, the Enter key will trigger the hyperlink as well. This implies that the control accepts focus.
  The parent window of the control will receive a notification after the link is launched.
  The control will obey left, right, and center justify styles (SS_LEFT, SS_RIGHT, and SS_CENTER).

For the hyperlink control, you’ll need to modify stock window behavior for fonts, painting, cursor display, keyboard input, and mouse clicks:

class CHyperlink: public CWnd
{
    public:
	CHyperlink();
	~CHyperlink();

    // Attributes
	public:
	COLORREF    m_rgbURLColor; // link display color
	CFont       m_fontControl; // link display font
	CRect       m_rectText;    // Text position
	HCURSOR     m_hcHand;      // Cursor for selecting

    // User API
    public:
	HINSTANCE ExecuteLink (void);
	inline COLORREF GetURLColor (void) const
	    { return (m_rgbURLColor); }
	inline void SetURLColor (COLORREF rgb)
	    { m_rgbURLColor = rgb; RecalcLayout(); }

    // Implementation
    protected:
	CFont* GetCorrectFont (void);
	void RecalcLayout (BOOL bRedraw = TRUE);
	void SendParentNotify(WORD wID, WORD wNotify) const;

    // Message Map
    public:
    //{{AFX_MSG(CHyperlink)
    afx_msg LRESULT OnSetFont (WPARAM wParam, LPARAM lParam);
    afx_msg LRESULT OnSetText (WPARAM wParam, LPARAM lParam);
    afx_msg void OnSize (UINT nType, int cx, int cy);
    afx_msg UINT OnGetDlgCode(void);
    afx_msg void OnPaint (void);
    afx_msg UINT OnNcHitTest (CPoint ptScreen);
    afx_msg BOOL OnSetCursor (CWnd* pWnd, UINT nHitTest, UINT message);
    afx_msg void OnSetFocus (CWnd* pwndOld);
    afx_msg void OnKillFocus (CWnd* pwndNew);
    afx_msg void OnLButtonDown(UINT /* nFlags */, CPoint ptMouse);
    afx_msg void OnLButtonUp    (UINT /* nFlags */, CPoint ptMouse);
    afx_msg void OnCancelMode (void);
    afx_msg void OnKeyDown (UINT nChar, UINT nRepCnt, UINT nFlags);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

Implementation Strategy

Before diving into the messages to process, take a look at the internal design of the class. The overall implementation strategy is to leave as much data and processing as possible to Windows default behavior. For this reason, you do not cache the text of the window, but rather use the GetWindowText API wherever needed.

You’ll monitor for specific messages that change the control’s display: text changes (WM_SETTEXT), font changes (WM_SETFONT), and sizing (WM_SIZE). If any of these messages are received, you use a catchall function, RecalcLayout, to recompute display metrics and optionally force the control to repaint. RecalcLayout (re)sets the text bounding rectangle (m_rectText) to the region of the control that contains the text. Note how this example handles the static styles (SS_LEFT, SS_RIGHT, and SS_CENTER) during rectangle calculation.

void CHyperlink::RecalcLayout (BOOL bRedraw)
{
    CString     strText;
    CRect       rectClient;
    CWindowDC   dc (this);
    DWORD       dwWindowStyle = GetStyle();

    // Compute the size of the text using the current font
    CFont* pfontControl = GetCorrectFont();
    CFont* pfontOld = dc.SelectObject (pfontControl);
    CSize sizeText = dc.GetTextExtent (strText);
    dc.SelectObject (pfontOld);

    GetClientRect (&rectClient);
    GetWindowText (strText);

if (dwWindowStyle & SS_CENTER)
    {
	m_rectText.SetRect (rectClient.Width()/2 - sizeText.cx / 2, 0,
			    rectClient.Width()/2 + sizeText.cx / 2,
			    sizeText.cy);
    }
    else if (dwWindowStyle & SS_RIGHT)
    {
	m_rectText.SetRect (rectClient.right - sizeText.cx, 0,
			       rectClient.right, sizeText.cy);
    }
    else // SS_LEFT is equal to zero
    {
	m_rectText.SetRect (0, 0, sizeText.cx, sizeText.cy);
    }

    if (bRedraw)
	Invalidate (TRUE);
}

Font Processing

The hyperlink control should fit into the overall layout of its containing dialog box as gracefully as possible. To do this, the control captures the message WM_SETFONT. Note that MFC does not provide a message-map entry for this message, so the raw-form ON_MESSAGE (WM_SETFONT, OnSetFont) syntax is used. Upon receipt of WM_SETFONT, the control retrieves the font characteristics, adds the underline style, and creates a new font. If the WM_SETFONT message is not received by the control, the control synthesizes a font from the font of its parent window.

CFont* CHyperlink::GetCorrectFont (void)
{
    // If the internal font is valid, skip processing
    if (m_fontControl.GetSafeHandle() == 0)
    {
       // Get parent window font for modification
CWnd* pwndParent = GetParent();
	if (pwndParent)
	{
	    LOGFONT logFont;
	    CFont* pfontParent = pwndParent->GetFont();

	    pfontParent->GetObject (sizeof (logFont),&logFont);
	    logFont.lfWidth  = 0;
	    logFont.lfQuality = PROOF_QUALITY;
	    logFont.lfUnderline = TRUE;

	    m_fontControl.CreateFontIndirect (&logFont);
	}
    }

    return (&m_fontControl);
}
LRESULT CHyperlink::OnSetFont (WPARAM wParam, LPARAM lParam)
{
    LOGFONT logFont;
    HFONT   hFont = (HFONT) wParam;

    Default(); // Pass message to default handler

    CFont* pfontDisplay = CFont::FromHandle (hFont);
    pfontDisplay->GetObject (sizeof (logFont), &logFont);
    logFont.lfUnderline = TRUE;
    logFont.lfWidth  = 0;
    logFont.lfQuality = PROOF_QUALITY;

    m_fontControl.DeleteObject();
    VERIFY (m_fontControl.CreateFontIndirect (&logFont));

    RecalcLayout();

    return (0L);
}

Notice how the RecalcLayout function is used to reset control metrics prior to exiting the WM_SETFONT message handler.



Painting the Window

Given the bounding rectangle for the text and the font, painting the control is rather trivial. The control allocates a paint DC, sets the font and text color, paints the text with a single DrawText call, and then resets the DC settings. When the control has the focus, DrawFocusRect is used to display a dotted rectangle around the text:

void CHyperlink::OnPaint()
{
    CString  strText;
    CPaintDC dc (this);

    // If we presently have the focus, remove it prior to painting
    if (GetFocus() == this)
	dc.DrawFocusRect (m_rectText);

    if (m_rectText.IsRectNull())
	RecalcLayout (FALSE);

    GetWindowText (strText);
    CFont* pfontControl = GetCorrectFont();
    CFont* pfontOld = dc.SelectObject (pfontControl);
    COLORREF rgbOldBk   = dc.SetBkColor (GetSysColor (COLOR_BTNFACE));
    COLORREF rgbOldText = dc.SetTextColor (m_rgbURLColor);

    CRect   rectClient;
    GetClientRect (rectClient);
    CBrush brushBack (GetSysColor (COLOR_BTNFACE));
    dc.FillRect (&rectClient, &brushBack);

    // Draw the text using the bounding rectangle from DrawText
    dc.DrawText (strText, m_rectText, DT_NOPREFIX|DT_SINGLELINE);

    dc.SetBkColor   (rgbOldBk);
    dc.SetTextColor (rgbOldText);
    dc.SelectObject (pfontOld);

    // If we have the focus, draw the standard focus rectangle
    // around the text
    if (GetFocus() == this)
	dc.DrawFocusRect (m_rectText);

    return;
}
void CHyperlink::OnSetFocus (CWnd* pwndOld)
{
    CWindowDC dc(this);
    dc.DrawFocusRect (m_rectText);
}

void CHyperlink::OnKillFocus (CWnd* pwndOld)
{
    CWindowDC dc(this);
    dc.DrawFocusRect (m_rectText);
}

To complete focus processing, catch the WM_SETFOCUS (OnSetFocus) and WM_KILLFOCUS (OnKillFocus) messages. As in the OnPaint handler, these message handlers invoke the DrawFocusRect to paint a highlight around the hyperlink text. The DrawFocusRect API uses an XOR drawing scheme, so the second call erases the rectangle drawn by the first one.

Be careful if your subclass relies on the default paint handler (that is, your class lets the control draw itself, and then overlays on top of the existing control). CPaintDC uses the Win32 BeginPaint and EndPaint APIs to allocate and release the device context. The EndPaint API has the normally beneficial side effect of revalidating the paint area. When your control allocates another CPaintDC, the drawing (clipping) area will be empty, and none of your output will appear. To overlay on top of an existing control’s output, use a CWindowDC instead. By default, the clipping area of a CWindowDC is the complete area of the window.

Controlling the Cursor

In most cases, the text of the hyperlink does not occupy the entire area of the window. When the cursor is over the text, you would like to display the traditional hand cursor that users associate with a hyperlink. When the cursor is over a whitespace area of the control, you display the arrow. To control the cursor, you catch and respond to the WM_SETCURSOR (OnSetCursor) message.

WM_SETCURSOR is passed to a window whenever a mouse event (move, click, and so on) happens over the client area of the window, or when the window has set input capture. The WM_SETCURSOR message includes the window under the cursor, the mouse location in screen coordinates, and the mouse message being processed. The handler converts the mouse coordinates from screen to client coordinates and compares it to the bounding rectangle of the hyperlink text. If the point is inside the text rectangle, the hand cursor is set; otherwise, the default (arrow) cursor is displayed.

BOOL CHyperlink::OnSetCursor (CWnd* pWnd, UINT nHitTest, UINT message)
{
    CPoint ptMouse;

    GetCursorPos (&ptMouse);
    ScreenToClient (&ptMouse);

    if (pWnd == this && m_rectText.PtInRect (ptMouse))
    {
	// If cursor has been loaded, don’t re-load.
	if (m_hcHand == (HCURSOR) 0)
	{
	    m_hcHand = LoadCursor (AfxGetInstanceHandle(),
			 	        MAKEINTRESOURCE (IDC_HANDCURSOR));

	    // If you get this assert, you forgot to include
	    // IDC_HANDCURSOR in your resources.
	    ASSERT (m_hcHand);
	}

	::SetCursor (m_hcHand);

	return (TRUE);
    }

    return (CWnd::OnSetCursor (pWnd, nHitTest, message));
}

After the WM_SETCURSOR handler was added, the initial version of the hyperlink control did not display the hand cursor. Some investigation with Spy++ revealed that the default window handler for WM_NCHITTEST does not specify that the window has a client area.

Windows sends a message, WM_NCHITTEST (non-client hit test), whenever the mouse is over any area of a window. The purpose of the message is to allow the window to identify the specific region under the mouse. In other words, the window can return that the mouse is over a border, menu or title bar, close or size box, and so on. Windows handles the cursor display (and suppresses WM_SETCURSOR messages) unless the return from WM_NCHITTEST indicates that the mouse is over the client area of the window.

To solve the problem, the control catches the WM_NCHITTEST message. If the mouse cursor is over the hyperlink text, HTCLIENT is returned. This informs Windows of the boundaries of the client area and causes the mouse cursor to display properly.

UINT CHyperlink::OnNcHitTest (CPoint ptScreen)
{
    CRect   rectClient;
    CPoint  ptClient (ptScreen);

    GetClientRect (rectClient);
    ScreenToClient (&ptClient);

    if (rectClient.PtInRect (ptClient))
	return (HTCLIENT);
    else
	return (CWnd::OnNcHitTest (ptScreen));
}

Mouse Input

Mouse handling for the hyperlink control follows the pushbutton model. When the mouse goes down in a pushbutton, the face of the button is depressed; however, if the mouse is moved outside of the button, the face returns to its normal position. The button action isn’t taken unless the mouse is released over the button face. Although the appearance of the hyperlink isn’t altered when the mouse is clicked, the program does wait until receiving a mouse-up before launching the hyperlink.

To accomplish this, you need to ensure that you receive a mouse-up event (WM_LBUTTONUP) to match the mouse-down event (WM_LBUTTONDOWN). This is done through the SetCapture() API. Normally, input goes to the window under the cursor (for mouse), or the window with focus (keyboard). After SetCapture is invoked, all mouse and keyboard inputs for the process are routed directly to the capturing window. Only one window in a process can have “capture” at any given time.

Setting capture on the mouse-down event ensures that the control will receive the mouse-up event, regardless of where it occurs. If the user clicks down on another window and clicks up in yours, the hyperlink will not launch.

void CHyperlink::OnLButtonDown(UINT /* nFlags */, CPoint ptMouse)
{
    if (m_rectText.PtInRect (ptMouse))
    {
       // Make us the focus window.
if (GetFocus() != this && (GetStyle() & WS_TABSTOP))
	    SetFocus();

	SetCapture();
    }

    return;
}

void CHyperlink::OnLButtonUp(UINT /* nFlags */, CPoint ptMouse)
{
    // If capture is set to us, then the mouse went down
    // over our window
    if (GetCapture() == this)
    {
	ReleaseCapture();

       // Verify that the user didn’t mouse up or another window
if (m_rectText.PtInRect (ptMouse))
	{
	    ExecuteLink ();

	    SendParentNotify ((WORD) ::GetWindowLong (GetSafeHwnd(),
			          GWL_ID), HL_LINKCLICKED);

	}
    }

    return;
}

void CHyperlink::SendParentNotify (WORD wID, WORD wNotify) const
{
    HWND hwParent = GetParent()->GetSafeHwnd();

    ::SendMessage (hwParent, WM_COMMAND,
		      MAKEWPARAM (wID, wNotify), (LPARAM) GetSafeHwnd());
}

void CHyperlink::OnCancelMode (void)
{
    if (GetCapture() == this)

       ReleaseCapture();

    CWnd::OnCancelMode();

    return;

}



Besides the mouse-down and mouse-up events, you also catch and process OnCancelMode (WM_CANCELMODE). Windows sends this message to any window that captured input through SetCapture when a new window must be displayed. For example, if another application is becoming active due to a system keypress or a message box that is being displayed, the Window manager sends the window capturing input a WM_CANCELMODE. This allows the window to cancel capture and release any other allocated resources (such as timers).

Keyboard Input

One of the original requirements for the hyperlink control was that it accept keyboard input. This enables the user to tab to and from the hyperlink, much as he would with any other editable control in a dialog box. Several actions are necessary to accomplish this feat.

Normal keyboard input is processed by catching the WM_KEYDOWN (OnKeyDown), WM_KEYUP (OnKeyUp), and WM_CHAR (OnChar) messages. Windows sends a key-down message when a key is pressed on the keyboard, and sends a key-up message when a key is released. Key-up and key-down messages are sent for all keystrokes—not only alphanumeric characters, but also Shift keys, Control keys, Caps Lock, and so on.

If your control is interested only in the resulting keypress, use the WM_CHAR message. WM_CHAR messages are generated from multiple WM_KEYDOWN messages. For example, to type a capital A, you hold down the Shift key and press the A key. Your application will receive a WM_KEYDOWN for both the Shift key (VK_SHIFT) and the A key. Finally, a WM_CHAR message is generated specifying the resulting A keypress. After the WM_CHAR, WM_KEYUP messages are generated for both the A and Shift keys.

In general, custom controls that accept input, like edit controls, need a mixture of key-down/key-up messages and WM_CHAR messages. The hyperlink control needs only the Enter key. When this key is pressed, the hyperlink will launch.

Unfortunately, the Enter key is a very special key to Windows. By default, this keypress is intercepted by the dialog manager and routed to the default button in a dialog box. Other keys intercepted include the Tab key (changes focus through the controls of a dialog box), the arrow keys (change the current control within a group), and mnemonics (used with the Alt key to jump to a specific control). To receive the Enter key (or any of the other special keys), you need to jump through some hoops.

The dialog manager is willing to concede these keys on an as-needed basis. To determine which keys a control requires, the dialog manager sends a WM_GETDLGCODE (OnGetDlgCode) message prior to sending a keyboard message. The return from the recipient window controls whether a specific keypress is intercepted or not.

Besides values for stock controls (buttons, edit controls, or static text controls), the following return values can be combined to make a valid WM_GETDLGCODE response:

WM_GETDLGCODE Constant Description

DLGC_WANTALLKEYS Window receives all WM_KEYDOWN/WM_KEYUP messages.
DLGC_WANTARROWS Window receives non-intercepted messages plus arrow keys.
DLGC_WANTCHARS Window receives all WM_CHAR messages.
DLGC_WANTMESSAGE Window receives each keyboard message to decide on a message-by-message basis.
DLGC_WANTTAB Window receives non-intercepted messages plus Tab messages.

Conspicuously absent from the list is a code to receive just the Enter key. Experimenting with the dialog code for default pushbuttons yielded no results, so the window is stuck with the catchall DLGC_WANTALLKEYS.

If your control receives all keyboard messages, you are on your own to handle navigation commands. That is, instead of relying on the dialog manager to process tabs, arrow keys, and Esc, you must handle them yourselves. This is reasonably simple to do.

When the hyperlink control receives the Enter key (VK_RETURN), the link is launched, and a notification is sent to the parent window. In response to the Esc key, the control masquerades as the Cancel button (IDCANCEL) and sends a button-clicked message to its parent window. Finally, if the Tab key is pressed, focus is moved forward or backward through any sibling windows to find the next one with the tabstop style set. This mimics the standard behavior of the dialog manager.

UINT CHyperlink::OnGetDlgCode(void)
{
    return (DLGC_WANTALLKEYS);
}

void CHyperlink::OnKeyDown (UINT nChar, UINT nRepCnt, UINT nFlags)
{
    if (nChar == VK_RETURN)
    {
	ExecuteLink ();

	SendParentNotify ((WORD) ::GetWindowLong (GetSafeHwnd(),
			  GWL_ID), HL_LINKCLICKED);
    }
    else if (nChar == VK_ESCAPE)
    {
	SendParentNotify (IDCANCEL, BN_CLICKED);
    }
    else if (nChar == VK_TAB)
    {
	CWnd*   pwndFocus = this;
	BOOL    bForwardTab = (GetKeyState (VK_SHIFT) & 0x8000) ?
	ÄFALSE : TRUE;

	// Find the next or previous window which has the
	// tabstop bit set.
	do
	{
	    pwndFocus = pwndFocus->GetWindow (((bForwardTab)
					      ? GW_HWNDNEXT :
					      GW_HWNDPREV));

	    if (!pwndFocus)
		pwndFocus = GetWindow (((bForwardTab)
					   ? GW_HWNDFIRST : GW_HWNDLAST));

	} while (pwndFocus != this &&
		(!pwndFocus->IsWindowEnabled() &&
		!(::GetWindowLong (pwndFocus->GetSafeHwnd(),
				      GWL_STYLE) & WS_TABSTOP)));

	if (pwndFocus)
	    pwndFocus->SetFocus ();
    }
    else
	CWnd::OnKeyDown (nChar, nRepCnt, nFlags);
}

Launching the Link

To launch the hyperlink, the hyperlink control retrieves the current window text and passes it to the Windows shell for execution, with the “open” opcode. So far, I’ve tested the hyperlink with http and ftp links, the mailto: command, and local document files (C:\My Documents\Readme.wri, for example). Any filename registered as a shell extension will work. A logical improvement would be to display one set of text, while executing another—this would hide ugly or complex URLs and present the user with a comprehensible interface.

HINSTANCE CHyperlink::ExecuteLink (void)
{
    HINSTANCE hInstReturn;
    CString strText;

    GetWindowText (strText);

    hInstReturn = ShellExecute(GetSafeHwnd(),
				_T(“open”),
				strText,
				NULL,
				NULL,
				SW_SHOWNORMAL);

    return (hInstReturn);
}

It’s quite a bit of code for a seemingly simple class.<



Advanced Custom Control Topics

Although the clock and hyperlink static controls demonstrate the basics of custom control design, there are some additional techniques that can be useful. One is a technique for accessing Windows messages during the creation of a custom control. I also present various methods for sending notifications from a custom control to its parent window in this section. This section also includes a discussion of how to use a custom control with the Visual Studio Resource Editor.

Subclassing Limitations

Choosing an existing window class has the advantage of overcoming some significant MFC problems dealing with Window creation and initialization.

The MFC message-map paradigm has some severe limitations in creating custom controls. The biggest problem is initialization. Normally, newly created windows are initialized by catching the WM_CREATE message. At this point, the window has text, size, and positioning information. When embedded in dialog boxes, however, MFC window classes do not always get a chance to process WM_CREATE messages.

When a dialog box is created, the dialog manager creates the new dialog window, and then creates any child windows in the dialog template. MFC can subclass the dialog box during its creation; however, the child windows are created without subclassing. The soonest the child windows can be subclassed is during the WM_INITDIALOG (OnInitDialog) message. However, by that time, the new child windows have all received their WM_CREATE, WM_SIZE, and several other important messages.

Of course, if you create your window directly, rather than from a dialog template, you will get WM_CREATE and the other “early” messages. A common technique for controls that require the early messages is to use a placeholder window, typically a static. The static is placed on the dialog template with the same position, size, style, and ID as the target window. Then, a member function in the target window class is called to gather the information from the static, destroy it, and create the actual window.

BOOL CHyperlink::CreateFromStatic(CWnd* pwndParent, UINT wID)
{
    BOOL bSuccess = FALSE;
    CWnd* pwndStatic = pwndParent->GetDlgItem (wID);

    if (pwndStatic != (CWnd*) 0)
    {
	DWORD dwStyle = pwnd->GetStyle();
	CString strText;
	CRect rect;

	// Get the window text
	pwndStatic->GetWindowText (strText);

	// Get the window position and convert from screen -> client
	pwndStatic->GetWindowRect (rect);
	pwndParent->ScreenToClient(rect);

	pwndStatic->DestroyWindow();

	bSuccess = CWnd::Create (NULL,
			   strText,
			   dwStyle,
			   rect,
			   pwndParent,
			   wID,
			   NULL);
     }

     return (bSuccess);
}

This is a point that causes outrage among Windows API developers. The non-MFC mechanism for creating a class is to register a window class (with a window procedure), and then create windows of that class by embedding them on a dialog box. Why doesn’t MFC support registering window classes and doing the same thing?

MFC does in fact support window class registration; however, after you’ve registered the class, it’s difficult to do anything with it. If you register a custom window procedure, you can handle early messages, but you lose the ability to use message maps and to associate instance-specific data (such as the members of a class object in memory) with a window handle. That negates most of the advantages of using MFC for window class development in the first place.

If you use AfxWndProc, you get the message mapping, but no messages until the window is subclassed. This isn’t an obvious point. In fact, each message sent to the window is received by AfxWndProc, but no message mapping can exist until somebody associates a class with the window. After the class is attached, messages are routed to the proper handlers. But you can’t really attach to the window until your dialog gains control (generally WM_INITDIALOG), and at that point the “early” messages have been received and discarded.

Notifications

Controls that accept input need to be able to communicate with their parent windows when the user acts. Well-known examples of this include the EN_CHANGE notification, sent by edit controls when the contents change, and the BN_CLICKED message sent by pushbuttons when activated.

Most built-in window classes use notifications of this ilk; they send a WM_COMMAND message to their parent, along with the control ID, the message code, and their window handle. The WPARAM contains the control ID and message code in the low and high words, respectively. The LPARAM contains the window handle (HWND, not CWnd) of the sending control.

Of course, this use of message parameters leaves no mechanism to send details. It is incumbent on the parent window to query for details if needed. The SendParentNotify function of the CHyperlink control automates sending these kinds of notifications.

If you need to send back detailed parameters, consider sending a message in the WM_USER range to the parent. That way, you have the WPARAM and LPARAM parameters available for data (or to act as pointers). Better still, use the WM_NOTIFY message. The more recent common controls (property sheet, tree control, and so on) use WM_NOTIFY to send data to their parent windows. WM_NOTIFY messages contain the window handle of the sending control in the WPARAM, and a pointer to a structure in the LPARAM. The structure varies from control to control, but all start with an NMHDR.

For the ultimate in notification, use a callback interface. Allow the parent window (or delegate) to register itself with the control via an interface class; when the control needs to send details back to the parent, it invokes one or more functions in the callback interface. Of course, using this mechanism makes your control unusable by non-C++ applications.

Using the Resource Editor with Custom Classes

The Visual Studio Resource Editor gives you the ability to place unknown window classes into a dialog template. To do this, you must know the registered class name and the style bits appropriate for the class. The style bits are set as a 32-bit hexadecimal number (no constants)—so be prepared to do some math.

Reevaluating the style bits causes no end of grief during maintenance. There isn’t a place to put any comments into the dialog template, and who can remember that 0×10830000 is equivalent to WS_TABSTOP, WS_GROUP, WS_BORDER, and WS_VISIBLE? It’s far easier to create the controls during OnInitDialog processing, using CWnd::Create and symbolic constants.

Summary

Creating a custom control can be a very simple process or an extremely complex one. Start by documenting the observable behavior of the control: response to input, output formats, and the effect of any style flags. If you can, base it off an existing control, such as an edit control or combo box. These controls have years of testing and improvement behind them.

If you must start from scratch, map from observable behavior to Windows messages. Which messages do you need to capture, and what does each signal to your control? What are the different states (such as capture) that the control supports?

Be sure to test your control in a variety of scenarios. For example, make sure that the control works both in a dialog box and on a standalone basis. Change the Windows color scheme to some absurd setting to ensure that the control paints properly. Check that the control resizes properly under very high and very low display resolutions.

Ensure that the control creation strategy is valid. Does the control require a ::CreateFromStatic member (or similar), or can it be subclassed from an existing dialog control?